<!--
基础的Spine渲染
背景：透明
版本：4.2
-->
<!--suppress ALL -->
<script setup lang="ts">
import { onMounted, onUnmounted, Ref, ref, watch } from 'vue'
import winResize from './winResize.ts'
import isUrl from './isUrl.ts'
import defaultSpine from './spine-webgl-4.2.min.js'

let spine
let spineWebgl: Ref<HTMLCanvasElement> = ref(<any>null)
let canvas
let ctx
let gl
let shader
let batcher
let mvp
let skeletonRenderer
let assetManager
let lastFrameTime
let skeletons: any = {}
let images: any = {}
let isAnimating = false
let animationId

const props = defineProps({
  spine: {
    type: Object,
    default: defaultSpine,
  },
  spineData: {
    type: [Array, Object],
    default: () => {
      return [
        {
          name: 'huahuo',
          pos: {
            x: 100,
            y: 450,
          },
          scale: 1,
          scaleX: 1,
          scaleY: 1,
          initialAnimation: 'animation',
          type: 'spine',
          path: '',
        },
      ]
    },
  },
  defaultWidth: {
    type: Number,
    default: 1920,
  },
  defaultHeight: {
    type: Number,
    default: 1080,
  },
  size: {
    type: Object,
    default: () => {
      return null
    }
  },
  assetsPath: {
    type: String,
    default: 'https://coss.crabapi.cn/march7th/src/assets/spine/', // '/src/assets/spine/'
  },
  resize: {
    type: [Function, Object],
    default: () => winResize,
  },
  defaultBg: {
    type: Array,
    default() {
      return [0, 0, 0, 0]
    },
  },
})
const emit = defineEmits(['start'])

function init() {
  canvas = spineWebgl.value
  canvas.width = window.innerWidth
  canvas.height = window.innerHeight
  let config = {alpha: true}
  ctx = new spine.ManagedWebGLRenderingContext(canvas, config)
  if (!ctx.gl) {
    setTimeout(init, 500)
    return
  }
  gl = ctx.gl

  shader = spine.Shader.newTwoColoredTextured(ctx)
  batcher = new spine.PolygonBatcher(ctx)
  mvp.ortho2d(0, 0, canvas.width - 1, canvas.height - 1)
  skeletonRenderer = new spine.SkeletonRenderer(ctx)
  assetManager = new spine.AssetManager(ctx, props.assetsPath)
  props.spineData?.forEach(item => {
    if (item.type === 'spine') {
      assetManager.loadText(item?.path?.json || `${item.name}.json`)
      assetManager.loadTextureAtlas(item?.path?.atlas || `${item.name}.atlas`)
    }
    if (item.type === 'image') {
      const img = new Image()
      img.crossOrigin = 'Anonymous'
      img.src = isUrl(item.src) ? item.src : props.assetsPath + item.src
      img.onload = () => {
        images[item.name] = img
      }
    }
  })
  requestAnimationFrame(load)
}

function reInit() {
  stopAnimation()
  if (!props.spineData?.length) {
    return
  }
  assetManager = new spine.AssetManager(ctx, props.assetsPath)
  props.spineData?.forEach(item => {
    if (item.type === 'spine') {
      assetManager.loadText(item?.path?.json || `${item.name}.json`)
      assetManager.loadTextureAtlas(item?.path?.atlas || `${item.name}.atlas`)
    }
    if (item.type === 'image') {
      const img = new Image()
      img.crossOrigin = 'Anonymous'
      img.src = isUrl(item.src) ? item.src : props.assetsPath + item.src
      img.onload = () => {
        images[item.name] = img
      }
    }
  })
  requestAnimationFrame(load)
}

function load() {
  if (assetManager.isLoadingComplete()) {
    props.spineData?.forEach(item => {
      if (item.type === 'spine') {
        skeletons[item.name] = loadSkeleton(item?.path?.json || `${item.name}.json`, item.initialAnimation, item.skin, item.otherAnimation)
      }
    })
    lastFrameTime = Date.now() / 1000
    spineResize()
    stopAnimation()
    startAnimation(render)
  } else {
    isAnimating = true
    animationId = requestAnimationFrame(load)
  }
}

function spineResize() {
  if (props.size) {
    canvas.width = props.size.width
    canvas.height = props.size.height
  } else {
    canvas.width = window.innerWidth
    canvas.height = canvas.width / props.defaultWidth * props.defaultHeight
  }
  mvp.ortho2d(0, 0, canvas.width, canvas.height)
  ctx.gl.viewport(0, 0, canvas.width, canvas.height)
}

function renderSkeleton(name: string, delta: any, skinName: string) {
  let skeleton = skeletons[name].skeleton
  let state = skeletons[name].state
  let premultipliedAlpha = skeletons[name].premultipliedAlpha

  let data = props.spineData?.find(item => item.name === name)

  // 设置缩放比例
  if (data.scale === 'auto') {
    const x = data.size?.x || data.size
    const y = data.size?.y || undefined
    if (typeof x === 'string' && x.includes('%')) {
      const x2 = canvas.width / 100 * parseFloat(x)
      skeleton.scaleX = x2 / skeleton.data.width
    } else {
      skeleton.scaleX = x / skeleton.data.width
    }
    if (!y && y !== 0) {
      // noinspection JSSuspiciousNameCombination
      skeleton.scaleY = skeleton.scaleX
    } else if (typeof y === 'string' && y.includes('%')) {
      const y2 = canvas.height / 100 * parseFloat(y)
      skeleton.scaleY = y2 / skeleton.data.height
    } else {
      skeleton.scaleY = y / skeleton.data.height
    }
  } else {
    let dScale = Number(data.scale) === 0 ? 0 : (Number(data.scale) || 1)
    skeleton.scaleX = Number(data.scaleX) || dScale
    skeleton.scaleY = Number(data.scaleY) || dScale
  }

  // 设置坐标位置
  if (typeof data.pos.x === 'string' && data.pos.x.includes('%')) {
    const x2 = canvas.width / 100 * parseFloat(data.pos.x)
    skeleton.x = x2 + canvas.width / 2
  } else {
    skeleton.x = parseFloat(data.pos.x) + canvas.width / 2
  }
  if (typeof data.pos.y === 'string' && data.pos.y.includes('%')) {
    const x2 = canvas.height / 100 * parseFloat(data.pos.y)
    skeleton.y = x2 + canvas.height / 2
  } else {
    skeleton.y = parseFloat(data.pos.y) + canvas.height / 2
  }

  // 设置皮肤
  if (skinName) {
    // 尝试通过名称找到皮肤
    let skin = skeleton.data.findSkin(skinName)
    if (skin) {
      skeleton.setSkin(skin)
      // 可选：确保所有插槽都更新到新皮肤
      skeleton.setSlotsToSetupPose()
    } else {
      const skins = skeleton?.data?.skins
      if (skins && skins[0]) {
        skeleton.setSkin(skins[0])
        // 可选：确保所有插槽都更新到新皮肤
        skeleton.setSlotsToSetupPose()
      }
    }
  } else {
    const skins = skeleton?.data?.skins
    if (skins && skins[0]) {
      skeleton.setSkin(skins[0])
      // 可选：确保所有插槽都更新到新皮肤
      skeleton.setSlotsToSetupPose()
    }
  }

  state.update(delta)
  state.apply(skeleton)
  skeleton.updateWorldTransform(spine.Physics.update)

  shader.setUniformi(spine.Shader.SAMPLER, 0)
  shader.setUniform4x4f(spine.Shader.MVP_MATRIX, mvp.values)
  skeletonRenderer.premultipliedAlpha = premultipliedAlpha
  skeletonRenderer.draw(batcher, skeleton)
}

function render() {
  if (!isAnimating) {
    return
  }

  let now = Date.now() / 1000
  let delta = now - lastFrameTime
  lastFrameTime = now

  gl.clearColor(...props.defaultBg)
  gl.clear(gl.COLOR_BUFFER_BIT)

  /*********** 绘制spine *************/
  shader.bind()
  shader.setUniformi(spine.Shader.SAMPLER, 0)
  shader.setUniform4x4f(spine.Shader.MVP_MATRIX, mvp.values)
  batcher.begin(shader)

  /*********** 绘制image *************/
  gl.disable(gl.DEPTH_TEST) // 根据实际需要调整
  gl.enable(gl.BLEND)
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)

  /*********** 绘制开始 *************/
  props.spineData?.forEach(item => {
    if (item.type === 'spine') {
      try {
        gl.useProgram(shader.program) // 恢复Spine着色器
        renderSkeleton(item.name, delta, item.skin || '')
      } catch (e) {
        stopAnimation()
      }
    }
    if (item.type === 'image' && images[item.name]) {
    }
  })

  batcher.end()
  shader.unbind()

  animationId = requestAnimationFrame(render)
}

function loadSkeleton(name: string, initialAnimation: string = 'animation', skin: string | undefined = undefined, otherAnimation: [any] = undefined) {
  if (skin === undefined) skin = 'default'
  let atlas = assetManager.require(name.replace(/\.(skel|json)/, '') + '.atlas')
  let atlasLoader = new spine.AtlasAttachmentLoader(atlas)
  let skeletonLoader = name.endsWith('.skel') ? new spine.SkeletonBinary(atlasLoader) : new spine.SkeletonJson(atlasLoader)
  skeletonLoader.scale = 1
  let skeletonData = skeletonLoader.readSkeletonData(assetManager.require(name))
  let skeleton = new spine.Skeleton(skeletonData)
  skeleton.setSkinByName(skin)
  let bounds = calculateSetupPoseBounds(skeleton)
  let animationStateData = new spine.AnimationStateData(skeleton.data)
  let animationState = new spine.AnimationState(animationStateData)
  animationState.setAnimation(0, initialAnimation, true)
  if (otherAnimation) {
    otherAnimation.forEach((item, index) => {
      item && animationState.setAnimation(index + 1, item, true)
    })
  }
  return {skeleton: skeleton, state: animationState, bounds: bounds, premultipliedAlpha: false}
}

function calculateSetupPoseBounds(skeleton) {
  skeleton.setToSetupPose()
  skeleton.updateWorldTransform(spine.Physics.update)
  let offset = new spine.Vector2()
  let size = new spine.Vector2()
  skeleton.getBounds(offset, size, [])
  return {offset: offset, size: size}
}

function startAnimation(fn) {
  if (!isAnimating) { // 只启动一次动画
    isAnimating = true // 更新标志
    fn() // 启动动画
    emit('start')
  }
}

function stopAnimation() {
  if (isAnimating) { // 如果动画正在进行
    cancelAnimationFrame(animationId) // 取消当前请求的动画帧
    isAnimating = false // 更新标志
  }
}

watch(() => [props.size?.width, props.size?.height], spineResize)

onMounted(() => {
  spine = props.spine
  mvp = new spine.Matrix4()
  init()
  // window.addEventListener('resize', spineResize)
})

onUnmounted(() => {
  // window.removeEventListener('resize', spineResize)
})

watch(() => props.spineData, () => {
  reInit()
})

watch(() => props.assetsPath, () => {
  reInit()
})
</script>

<template>
  <canvas ref="spineWebgl"></canvas>
</template>
